Docker for Mac の Bridgeモードを考察してみた
Docker for Mac の Bridgeモードを考察してみた
はじめに
【 大阪オフィス開設1周年勉強会 】第6回 本番で使うDocker勉強会 in 大阪 2017/06/09 #cm_osaka | Developers.IO に参加した際、内輪で Docker の Bridge モードと Host モードの違いは何? という話題になりました。その時は、ざっくり言うと Network Namespace の使用有無であり、Network Namespace を利用する場合 iptables や Linux ブリッジ、veth pair (L2 トンネル)を使って、IP を転送しています。と説明した記憶があるのですが、本記事で少し考察してみます。
下準備
なお今回は、手元の Docker for Mac (moby linux)を利用して Bridge モードの考察を行います。Docker for Mac 環境で moby linux にアクセスする方法は、過去の記事に記載しておりますので参照してください。
筆者の検証環境(macOS と Docker for Mac)は、以下のとおりです。
~ $ docker version Client: Version: 17.03.1-ce API version: 1.27 Go version: go1.7.5 Git commit: c6d412e Built: Tue Mar 28 00:40:02 2017 OS/Arch: darwin/amd64 Server: Version: 17.03.1-ce API version: 1.27 (minimum version 1.12) Go version: go1.7.5 Git commit: c6d412e Built: Fri Mar 24 00:00:50 2017 OS/Arch: linux/amd64 Experimental: true ~ $ sw_vers ProductName: Mac OS X ProductVersion: 10.12.5 BuildVersion: 16F73 ~ $
次に、moby linux 側で Network Namespace を確認するために、iproute2 パッケージをインストールしておきます。
/ # apk update / # apk add iproute2
Bridgeモード
Bridgeモードは、Hypervisor 側の Linux に構成されている docker0 という Linux Bridge を介してコンテナ起動時に veth pair(Virtual Ethernet Tunnel)が作成され、片方は docker0 側へ接続、もう片方は起動されたコンテナの Network Namespace 内に eth0 として接続されることで仮想的なネットワーク環境を構成します。
まずは、コンテナが起動されていない状態のネットワークを確認してみます。
/ # ip addr show dev docker0 14: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN link/ether 02:42:6d:7e:c4:d2 brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 scope global docker0 valid_lft forever preferred_lft forever / # brctl show bridge name bridge id STP enabled interfaces docker0 8000.02426d7ec4d2 no / # docker network inspect bridge [ { "Name": "bridge", "Id": "0ef4bececf3170d4e2c6822f5e7560d355cc8be84f5988030887237f38438b96", "Created": "2017-06-23T01:17:12.801021267Z", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" } ] }, "Internal": false, "Attachable": false, "Containers": {}, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} } ] / # iptables -S -P INPUT ACCEPT -P FORWARD DROP -P OUTPUT ACCEPT -N DOCKER -N DOCKER-ISOLATION -A FORWARD -j DOCKER-ISOLATION -A FORWARD -o docker0 -j DOCKER -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -i docker0 ! -o docker0 -j ACCEPT -A FORWARD -i docker0 -o docker0 -j ACCEPT -A DOCKER-ISOLATION -j RETURN / # iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy DROP) target prot opt source destination DOCKER-ISOLATION all -- anywhere anywhere DOCKER all -- anywhere anywhere ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED ACCEPT all -- anywhere anywhere ACCEPT all -- anywhere anywhere Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain DOCKER (1 references) target prot opt source destination Chain DOCKER-ISOLATION (1 references) target prot opt source destination RETURN all -- anywhere anywhere / #
では、次に ubuntu コンテナを起動した直後の状態を確認します。
/ # docker run -it --name ubuntu bash bash-4.4# exit / # docker start ubuntu ubuntu
exit でコンテナから一旦抜け、再度 ubuntu コンテナを起動しておき Linux Bridge を確認してみます。
/ # brctl show bridge name bridge id STP enabled interfaces docker0 8000.02426d7ec4d2 no vethef1372a / # ip addr show dev vethef1372a 27: vethef1372a@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default link/ether da:e5:98:49:86:5b brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fe80::d8e5:98ff:fe49:865b/64 scope link valid_lft forever preferred_lft forever
veth pair の片側が、docker0 に接続されています。 もう片側の構成を確認する前に、docker network を確認しておきます。
/ # docker network inspect bridge [ { "Name": "bridge", "Id": "0ef4bececf3170d4e2c6822f5e7560d355cc8be84f5988030887237f38438b96", "Created": "2017-06-23T01:17:12.801021267Z", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" } ] }, "Internal": false, "Attachable": false, "Containers": { "be4f826c8090297f8d85d56a4d75e134932f15eb48cdae6dba1378cf8d445669": { "Name": "ubuntu", "EndpointID": "6c3ff09fa947e17463a05c46590661379216607b6829f0bf47c40e471609f89a", "MacAddress": "02:42:ac:11:00:02", "IPv4Address": "172.17.0.2/16", "IPv6Address": "" } }, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} } ]
MAC アドレス "02:42:ac:11:00:02" が割当てられた仮想 NIC (veth pair のもう片方)が、eth0 として構成されているはずです。では、Network Namespace 内を覗いてみましょう。その前に、少し下準備が必要です。
ubuntu コンテナで動作している bash のプロセスID を確認し、Network Namespace を操作するための FD(File Descripter)を確認し、/var/run/netns 配下にシンボリックリンクを張っておきます。
/ # ps aux | grep bash 9356 root 0:00 bash / # pstree init-+-acpid |-chronyd |-containerd---containerd-shim---tini---rngd |-containerd-ctr |-crond |-dhcpcd |-diagnostics-ser |-klogd |-proxy-vsockd |-sh-+-dockerd---docker-containe---docker-containe---bash | `-logger |-sh---pstree |-syslogd |-transfused `-vsudd / # ls -l /proc/9356/ns/net lrwxrwxrwx 1 root root 0 Jun 28 01:38 /proc/9356/ns/net -> net:[4026532310] / # mkdir /var/run/netns / # ln -s /proc/9356/ns/net /var/run/netns/ubuntu / # ip netns ubuntu (id: 0)
準備が整いました。では、確認します。 以下のコマンドにより、ubuntu コンテナの Network Namespace 内で ip コマンドを実行し eth0 の情報を確認します。
/ # ip netns exec ubuntu ip addr show dev eth0 26: eth0@if27: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 172.17.0.2/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::42:acff:fe11:2/64 scope link valid_lft forever preferred_lft forever / #
docker network inspect bridge で確認したとおりの仮想NIC(eth0)が構成されています。 つまり、このコンテナ内の eth0@if27 は docker0 に構成された vethef1372a@if26 の相棒です。 ここまでで、Linux Bridge(docker0)と、veth pair および Network Namespace について見てきました。
次に、nginx コンテナを起動し -p 8080:80 のようにポートフォーワードを行った場合、どのように変化があるか 見てみます。
まずは、nginx コンテナを起動します。
/ # docker run -d -p 8080:80 --name nginx nginx 2c8e2dcf5a9d834cf957fe703b7f13207840bd6746d95f223d747b75e410bb20 / # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2c8e2dcf5a9d nginx "nginx -g 'daemon ..." 6 seconds ago Up 5 seconds 0.0.0.0:8080->80/tcp nginx
次に、moby linux 側で 8080 ポートを LISTEN しているプロセスと、iptables を確認します。
/ # netstat -anp | egrep '8080|Local Address' Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 :::8080 :::* LISTEN 10083/slirp-proxy / # ps -ef | grep 10083 10083 root 0:00 /usr/bin/slirp-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80
slirp-proxy というプロセスが 8080 を待ち受けていました。 プロセスの引数から、moby linux 宛の TCP/8080 を nginx(172.17.0.2:80) へフォーワードするようです。
/ # iptables -S -P INPUT ACCEPT -P FORWARD DROP -P OUTPUT ACCEPT -N DOCKER -N DOCKER-ISOLATION -A FORWARD -j DOCKER-ISOLATION -A FORWARD -o docker0 -j DOCKER -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -i docker0 ! -o docker0 -j ACCEPT -A FORWARD -i docker0 -o docker0 -j ACCEPT -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT ★ -A DOCKER-ISOLATION -j RETURN / # iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy DROP) target prot opt source destination DOCKER-ISOLATION all -- anywhere anywhere DOCKER all -- anywhere anywhere ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED ACCEPT all -- anywhere anywhere ACCEPT all -- anywhere anywhere Chain OUTPUT (policy ACCEPT) target prot opt source destination Chain DOCKER (1 references) target prot opt source destination ACCEPT tcp -- anywhere 172.17.0.2 tcp dpt:http ★ Chain DOCKER-ISOLATION (1 references) target prot opt source destination RETURN all -- anywhere anywhere
こちらは、nginx コンテナ起動前と比較して、星印の行が追加されています。 nginx コンテナ(TCP/80)宛てのパケット転送を許可しているものと思われます。(少し自信がない)
駆け足で、Bridge モード時のネットワーク構成について確認しましたが何となくイメージは掴めたでしょうか。 このあたりの docker コンテナと Network Namespace に関するお話は、以下の記事が凄く参考になると思います。 ご一読頂ければ、幸いです。
なお、Host モードの場合、上記に記載された docker0 ブリッジ経由での(veth pair を介する)通信や IP 転送などが不要となります。
/ # docker run -d --name nginx-host-mode --net=host nginx ad8cf0dd02dad80555cc2705fb87e94ca63fd8b0ffe6855b2400c7e5a990ca08 / # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ad8cf0dd02da nginx "nginx -g 'daemon ..." About a minute ago Up About a minute nginx-host-mode / # netstat -anp | egrep '80|Local Address' Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 10255/nginx: master
moby linux 側の TCP/80 で nginx プロセスが待ち受けていますね。
さいごに
Docker for Mac のBridgeモードについて考察してみました。ECS インスタンス(例えば、Amazon Linux 上の Docker)環境と比較した場合、moby linux のBridgeモードとは、少し実装が異なるかもしれません。(まだ、未確認) しかしながら、BridgeモードとHostモードを比較すると少なからずソフトウェアオーバーヘッドの差異が表れるということは、これまでの説明により理解頂けるのではないかと考えております。
機会があれば、Bridge vs Host モードでベンチマークテストを試してみたいなと考えています。 ではでは